/**
 * Copyright (C) 2010 altuure <altuure [AT] gmail [DOT] com> http://www.altuure.com/projects/yagdao
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *         http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.altuure.yagdao.jpa;

import com.altuure.yagdao.*;
import com.altuure.yagdao.common.*;
import com.altuure.yagdao.jpa.JpaGrammarHelper.PredicateResult;
import com.altuure.yagdao.jpa.JpaGrammarHelper.SelectList;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import javax.persistence.*;
import javax.persistence.NoResultException;
import javax.persistence.criteria.*;
import java.io.Serializable;
import java.lang.annotation.Annotation;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.*;

/**
 * generic DAO factory for JPA.
 * @author altuure
 */
public class GenericJpaDAOFactory extends GenericDAOFactory {
    private static final Log log= LogFactory.getLog(GenericJpaDAOFactory.class);

    /**
     * created DAO bean instance
     */
    protected GenericJpaDAO<Object, Serializable> genericDAO;
    /**
     * injected entity manager
     */
    private EntityManagerAccessor entityManagerAccessor;

    public GenericJpaDAOFactory() {
        setTargetDAOImpl(GenericJpaDAO.class);
    }

    public Object getObject() throws Exception {
        return genericDAO;
    }

    /**
     * create proxy class
     */
    public void initClass() {
        InvocationHandler h = initHandler();
        if (h == null)
            return;
        genericDAO = (GenericJpaDAO<Object, Serializable>) Utils.createInstance(h, targetDAOImpl, targetInterfaces);
        this.daoClass = genericDAO.getClass();
        initObjectClass();

    }

    /**
     * init DAO bean
     * @throws Exception init exception
     */
    protected void initDAO() throws Exception {
        initClass();
        genericDAO.setObjectClass(objectClass);
        genericDAO.setEntityManagerAccessor(entityManagerAccessor);
        genericDAO.init();
    }

    /*
     * (non-Javadoc)
     * 
     * @see
     * com.altuure.yagdao.common.GenericDAOFactory#criteriaMethod(java.lang.
     * Object, java.lang.reflect.Method, java.lang.Object[])
     */
    protected Object criteriaMethod(Object obj, Method method, Object[] args) {

        CriteriaBuilder criteriaBuilder = getEntityManager().getCriteriaBuilder();
        CriteriaQuery<Object> criteriaQuery = criteriaBuilder.createQuery();
        Root from = criteriaQuery.from(objectClass);
        CriteriaQuery<Object> select = selectResult(criteriaQuery, from, method,criteriaBuilder);
        YPage paging = findDAOPage(method, args);

        Map<String, From> joins = setPredicates(method, args, from, criteriaBuilder, criteriaQuery);
        fetchStatements(from, method);
        setGroupbyStatements(method,from,criteriaBuilder,joins,criteriaQuery);
        /*
         * if (paging == null) { TypedQuery<Object> typedQuery =
         * getEntityManager().createQuery(select); return new
         * SearchResultList((List) typedQuery.getResultList()); }
         */
        genericDAO.setOrders(select, paging, criteriaBuilder, from, joins);
        TypedQuery<Object> typedQuery = getEntityManager().createQuery(select);
        genericDAO.setLimits(typedQuery, paging);

        if (SearchResultList.class.isAssignableFrom(method.getReturnType())) {
            long totalCount = (Long) countMethod(obj, method, args);
            return new SearchResultList(typedQuery.getResultList(), totalCount, paging);
        } else if(Collection.class.isAssignableFrom(method.getReturnType())) {
            return typedQuery.getResultList();
        }else{
            return typedQuery.getFirstResult();
        }


    }

    /**
     * append method handler
     * @param obj object
     * @param method method
     * @param args argument array
     * @return  query result
     */
    @Override
    protected Object appendMethod(Object obj, Method method, Object[] args) {
        log.debug("GenericHibernateDAOFactory.appendMethod");
        final boolean countResults = SearchResultList.class.isAssignableFrom(method.getReturnType());

        YPage paging = findDAOPage(method, args);
        final AppendQuery appendQuery = buildQuery(obj, method, args, genericDAO.getObjectClass(),true);
        StringBuffer orderPart = new StringBuffer();
        if(!paging.getOrders().isEmpty()){
            List<YOrder> orders = paging.getOrders();
            orderPart.append(" order by ");
            boolean  first=true;
            for (YOrder order : orders) {
                if(!first)
                    orderPart.append(" , ");
                first=false;

                orderPart.append("this.").append(order.getName()).append(" ").append(order.getSort()).append(" ");
            }
        }
        String queryString = appendQuery.getSelect() + appendQuery.getFrom()+orderPart.toString();
        Query query = getEntityManager().createQuery(queryString);
        //setQueryParameters(method, appendQuery.getParameters().toArray(), query,false);
        final List<Object> parameters = appendQuery.getParameters();
        for (int i = 0; i < parameters.size(); i++) {
            query.setParameter(i+1,parameters.get(i));

        }



        if(countResults){
            final AppendQuery appendQuery2 = buildQuery(obj, method, args, genericDAO.getObjectClass(),false);
              Query countQuery = getEntityManager().createQuery("select count(*) "+appendQuery2.getFrom());
            for (int i = 0; i < parameters.size(); i++) {
                countQuery.setParameter(i+1,parameters.get(i));
            }
            final Number count = (Number) countQuery.getSingleResult();
            genericDAO.setLimits(query, paging);
             return new SearchResultList(query.getResultList(), count.intValue(),paging);
        }else if(Collection.class.isAssignableFrom(method.getReturnType())) {
           return genericDAO.query(query, paging);
        }else{
            try {
                return query.getSingleResult();
            } catch (NoResultException e) {
                throw new com.altuure.yagdao.common.NoResultException(e);
            }
        }
        

    }


    /**
     *  append group by statement
     * @param method  executed method
     * @param from root
     * @param criteriaBuilder active builder
     * @param joins joins cache
     * @param criteriaQuery cq
     */
    private void setGroupbyStatements(Method method, Root from, CriteriaBuilder criteriaBuilder, Map<String, From> joins,
            CriteriaQuery<Object> criteriaQuery) {
        YMethod yMethod = method.getAnnotation(YMethod.class);
        if (yMethod != null&&Utils.isNotEmpty(yMethod.groupBy())) {
                List<Expression> predicates = JpaGrammarHelper.getGroupsByStatements(yMethod.groupBy(), from, criteriaBuilder, joins);
                criteriaQuery.groupBy(predicates.toArray(new Expression[predicates.size()]));

            
        }
        
    }

    /**
     * get entity manager from accessor
     * @return  entitymanager
     */
    private EntityManager getEntityManager() {
        return entityManagerAccessor.getEntityManager();
    }

    /**
     * @param method handled method
     * @param args arguments
     * @param from root
     * @param criteriaBuilder criteria builder
     * @param criteriaQuery cq
     * @return predicate list
     */
    public Map<String, From> setPredicates(Method method, Object[] args, Root from, CriteriaBuilder criteriaBuilder,
                                           CriteriaQuery<Object> criteriaQuery) {
        Map<String, From> joins = new HashMap<String, From>();
        Predicate rootPredicate = null;
        List<Predicate> havingStatements = new ArrayList<Predicate>();
        Annotation[][] parameterAnnotations = method.getParameterAnnotations();
        YMethod yMethod = method.getAnnotation(YMethod.class);
        if (yMethod != null) {
            if (Utils.isNotEmpty(yMethod.where())) {
                rootPredicate = JpaGrammarHelper.whereStatements(yMethod.where(), from, criteriaBuilder, joins);
            }
            if (Utils.isNotEmpty(yMethod.having())) {
                Predicate havingStatements2 = JpaGrammarHelper.havingStatements(yMethod.having(), from, criteriaBuilder, joins);
                havingStatements.add(havingStatements2);
            }

        }
        for (int i = 0; i < parameterAnnotations.length; i++) {
            Annotation[] parameterAnnotation = parameterAnnotations[i];
            if (parameterAnnotation == null)
                continue;
            Object value = args[i];

            /*  TODO check for empty string
          if (value instanceof String) {
                String s = (String) value;
                if (Utils.isEmpty( s))
                    continue;
            }*/
            YParameter daoMethodParameter = null;
            for (Annotation annotation1 : parameterAnnotation) {
                if (annotation1 instanceof YParameter) {
                    daoMethodParameter = (YParameter) annotation1;
                    break;
                }
            }
            if (daoMethodParameter == null)
                continue;
            if (value == null && !daoMethodParameter.acceptNull())
                continue;

            YOperator operator = daoMethodParameter.operator();

            String property = daoMethodParameter.value();

            Predicate predicate = null;
            if (Utils.isNotEmpty(property)) {
                PredicateResult tempPredicate = JpaGrammarHelper.addPredicate(property, from, criteriaBuilder,
                        joins, value, operator);
                if (tempPredicate.isHaving()) {
                    Predicate predicate2 = tempPredicate.getPredicate();
                    if (daoMethodParameter.not())
                        predicate2 = predicate2.not();
                    havingStatements.add(predicate2);
                } else
                    predicate = tempPredicate.getPredicate();
            }
            if (predicate != null) {
                if (daoMethodParameter.not())
                    predicate = predicate.not();
                if (rootPredicate == null) {
                    rootPredicate = predicate;
                } else {
                    rootPredicate = criteriaBuilder.and(rootPredicate, predicate);
                }
            }

        }


        if (rootPredicate != null)
            criteriaQuery.where(rootPredicate);
        if (!havingStatements.isEmpty())
            criteriaQuery.having(havingStatements.toArray(new Predicate[0]));

        return joins;
    }

    /**
     * selct root object or selected fields
     * @param criteriaQuery query
     * @param from from root
     * @param method executed method
     * @param criteriaBuilder 
     * @return selection
     */
    private CriteriaQuery<Object> selectResult(CriteriaQuery<Object> criteriaQuery, Root from, Method method, CriteriaBuilder criteriaBuilder) {
        YMethod annotation = method.getAnnotation(YMethod.class);
        if (annotation == null || annotation.select() == null || Utils.isEmpty(annotation.select()))
            return criteriaQuery.select(from);

        String select = annotation.select();
        SelectList selectlist = JpaGrammarHelper.getSelectionList(select, from, criteriaBuilder, new HashMap<String, From>());
        List<Expression> selectionList;
        selectionList =selectlist.getExpressions() ;
        return criteriaQuery.multiselect(selectionList.toArray(new Selection[0])).distinct(selectlist.isDistinct());

    }

    /**
     * add fetch statements for select
     * @param from root from
     * @param method target methods
     */
    private void fetchStatements(Root from, Method method) {
        YFetch daoFetch = method.getAnnotation(YFetch.class);
        if (daoFetch == null)
            daoFetch = (YFetch) objectClass.getAnnotation(YFetch.class);
        if (daoFetch != null) {
            String[] fetchProperties = daoFetch.value();
            HashMap<String, FetchParent> fetchs = new HashMap<String, FetchParent>();
            for (String s : fetchProperties) {
                String[] split = Utils.splitDot(s);

                genericDAO.getLatestFetch(from, fetchs, split);

            }

        }
    }

    /*
     * (non-Javadoc)
     * 
     * @see
     * com.altuure.yagdao.common.GenericDAOFactory#queryMethod(java.lang.Object,
     * java.lang.reflect.Method, java.lang.Object[])
     */
    protected Object queryMethod(Object obj, Method method, Object[] args) {
        YMethod daoMethod = method.getAnnotation(YMethod.class);

        YLimit paging = findDAOPage(method, args);

        Query query = createQuery(daoMethod);
        setQueryParameters(method, args, query,true);
        @SuppressWarnings("rawtypes")
        SearchResultList resultList = genericDAO.query(query, paging);
        if (objectClass.equals(method.getReturnType())) {
            if (resultList.isEmpty())
                return null;
            if (resultList.size() == 1)
                return resultList.get(0);
            else
                throw new NonUniqueResultException("total size:" + resultList.size());
        }

        return resultList;

    }

    /**
     * Set query parameters from query parameter annotaions
     * @param method Query method
     * @param args arguments
     * @param query query
     * @param useName use parameter names
     */
    private void setQueryParameters(Method method, Object[] args, Query query,boolean useName) {
        final Annotation[][] parameterAnnotations = method.getParameterAnnotations();
        for (int i = 0; i < parameterAnnotations.length; i++) {
            final Annotation[] annotations = parameterAnnotations[i];
            final Object arg = args[i];
            YParameter daoMethodParameter = null;
            for (final Annotation annotation1 : annotations) {
                if (annotation1 instanceof YParameter) {
                    daoMethodParameter = (YParameter) annotation1;
                    break;
                }
            }
            // check if parameter if mapped
            if (daoMethodParameter != null&&useName) {
                final String name = daoMethodParameter.value();
                query.setParameter(name, arg);
            } else if (!(arg instanceof YLimit)) {
                query.setParameter(i + 1, arg);
            }

        }
    }

    /**
     * Create Query from method annotations
     * @param daoMethod method annotation
     * @return query from annotations
     */
    private Query createQuery(YMethod daoMethod) {
        Query query = null;
        if (!Utils.isEmpty(daoMethod.query()))
            query = getEntityManager().createQuery(daoMethod.query());
        else if (!Utils.isEmpty(daoMethod.queryName())) {
            query = getEntityManager().createNamedQuery(daoMethod.queryName());
        } else {
            throw new IllegalArgumentException("queryName or query must be defined");
        }
        return query;
    }

    protected Object saveMethod(Object obj, Method method, Object[] args) throws InstantiationException,
            IllegalAccessException, InvocationTargetException, NoSuchMethodException {

        Object o = objectClass.newInstance();
        setValues(o, method, args);
        getEntityManager().persist(o);
        return o;
    }

    @Override
    public YPage findDAOPage(Method method, Object[] args) {
        return super.findDAOPage(method, args); // To change body of overridden
                                                // methods use File | Settings |
                                                // File Templates.
    }

    protected Object updateMethod(Object obj, Method method, Object[] args) throws InstantiationException,
            IllegalAccessException, InvocationTargetException, NoSuchMethodException {
        Object o = genericDAO.load((Serializable) args[0]);
        setValues(o, method, args);
        getEntityManager().merge(o);
        getEntityManager().flush();
        return o;
    }

    /**
     * execute update statement
     * @param obj proxy object
     * @param method method
     * @param args arguments
     * @return return number of updated columns
     * @throws Throwable
     */
    protected Object executeMethod(Object obj, Method method, Object[] args) throws Throwable {
        YMethod daoMethod = method.getAnnotation(YMethod.class);

        Query query = createQuery(daoMethod);
        setQueryParameters(method, args, query,true);
        return query.executeUpdate();
    }

    protected Object countMethod(Object obj, Method method, Object[] args) {
        CriteriaBuilder criteriaBuilder = getEntityManager().getCriteriaBuilder();
        CriteriaQuery<Object> criteriaQuery = criteriaBuilder.createQuery();
        Root from = criteriaQuery.from(objectClass);

        setPredicates(method, args, from, criteriaBuilder, criteriaQuery);

        Expression<Long> minExpression = criteriaBuilder.count(from.get(genericDAO.getIdProperty()));

        CriteriaQuery<Object> select = criteriaQuery.select(minExpression);

        TypedQuery<Object> typedQuery = getEntityManager().createQuery(select);

        return typedQuery.getSingleResult();

    }

    protected Object deleteMethod(Object obj, Method method, Object[] args) {
        Object o = getEntityManager().getReference(objectClass, (Serializable) args[0]);
        getEntityManager().remove(o);
        return o;
    }

    public void setObjectClass(Class objectClass) {
        this.objectClass = objectClass;
    }

    public String toString() {
        return GenericJpaDAOFactory.class.getName() + ":" + objectClass;
    }

    public void setEntityManagerAccessor(EntityManagerAccessor entityManagerAccessor) {
        this.entityManagerAccessor = entityManagerAccessor;

    }

    /**
     * create new Instance of given interface or abstract class
     * @param clz interface
     * @param entityManagerAccessor utility_
     * @return instance of given interface
     * @throws Exception creation exception
     */
    public static Object createInstance(Class clz, EntityManagerAccessor entityManagerAccessor) throws Exception {
        GenericJpaDAOFactory daoFactory = new GenericJpaDAOFactory();
        daoFactory.setTargetInterfaces(clz);
        daoFactory.setEntityManagerAccessor(entityManagerAccessor);
        daoFactory.initDAO();
        return daoFactory.getObject();
    }

}
